﻿// MIT License
// 开源地址：https://gitee.com/co1024/AbcMvc
// Copyright (c) 2021-2023 1024
// Abc.Mvc=Furion+EF+SqlSugar+Pear layui admin.

using MailKit;
using System.Globalization;

namespace Abc.Utils;

public class CustomProtocolLogger : IProtocolLogger
{
    private static byte[] defaultClientPrefix = Encoding.ASCII.GetBytes("C: ");
    private static byte[] defaultServerPrefix = Encoding.ASCII.GetBytes("S: ");
    private static readonly byte[] Secret = Encoding.ASCII.GetBytes("********");
    private static readonly byte[] Space = new byte[] { (byte)' ' };

    private const string DefaultTimestampFormat = "yyyy-MM-ddTHH:mm:ssZ";

    private byte[] clientPrefix = defaultClientPrefix;
    private byte[] serverPrefix = defaultServerPrefix;
    private readonly Stream stream;
    private readonly bool leaveOpen;
    private bool clientMidline;
    private bool serverMidline;

    private CustomProtocolLogger()
    {
        TimestampFormat = DefaultTimestampFormat;
        RedactSecrets = true;
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="MailKit.ProtocolLogger"/> class.
    /// </summary>
    /// <remarks>
    /// Creates a new <see cref="ProtocolLogger"/> to log to a specified file. The file is created if it does not exist.
    /// </remarks>
    /// <example>
    /// <code language="c#" source="Examples\SmtpExamples.cs" region="ProtocolLogger" />
    /// </example>
    /// <param name="fileName">The file name.</param>
    /// <param name="append"><c>true</c> if the file should be appended to; otherwise, <c>false</c>. Defaults to <c>true</c>.</param>
    public CustomProtocolLogger(string fileName, bool append = true) : this()
    {
        //如果出现异常，重新生成新的文件名
        for (int i = 0; i < 5; i++)
        {
            try
            {
                stream = File.Open(fileName, append ? FileMode.Append : FileMode.Create, FileAccess.Write, FileShare.Read);

                break;
            }
            catch (Exception ex)
            {
                fileName = GetNewFilePath(fileName);
                //线程休眠3秒后再执行
                Thread.Sleep(3 * 1000);
            }
        }
    }

    private string GetNewFilePath(string fileName)
    {
        var dir = AppDomain.CurrentDomain.BaseDirectory + $"\\Logs\\SendEmailLogs\\{DateTimeOffset.Now:yyyyMM}\\";
        if (!Directory.Exists(dir))
        {
            Directory.CreateDirectory(dir);
        }
        //判断文件是否存在
        //if (File.Exists(fileName))
        {
            var extentsionName = fileName.GetExtension();

            fileName = $"{fileName.Replace(extentsionName, "")}_{DateTimeOffset.Now.Ticks}{extentsionName}";
        }

        return fileName;
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="MailKit.ProtocolLogger"/> class.
    /// </summary>
    /// <remarks>
    /// Creates a new <see cref="ProtocolLogger"/> to log to a specified stream.
    /// </remarks>
    /// <param name="stream">The stream.</param>
    /// <param name="leaveOpen"><c>true</c> if the stream should be left open after the protocol logger is disposed.</param>
    public CustomProtocolLogger(Stream stream, bool leaveOpen = false) : this()
    {
        if (stream == null)
            throw new ArgumentNullException(nameof(stream));

        this.leaveOpen = leaveOpen;
        this.stream = stream;
    }

    /// <summary>
    /// Releases unmanaged resources and performs other cleanup operations before the <see cref="MailKit.ProtocolLogger"/>
    /// is reclaimed by garbage collection.
    /// </summary>
    /// <remarks>
    /// Releases unmanaged resources and performs other cleanup operations before the <see cref="MailKit.ProtocolLogger"/>
    /// is reclaimed by garbage collection.
    /// </remarks>
    ~CustomProtocolLogger()
    {
        Dispose(false);
    }

    /// <summary>
    /// Get the log stream.
    /// </summary>
    /// <remarks>
    /// Gets the log stream.
    /// </remarks>
    /// <value>The log sstream.</value>
    public Stream Stream
    {
        get { return stream; }
    }

    /// <summary>
    /// Get or set the default client prefix to use when creating new <see cref="MailKit.ProtocolLogger"/> instances.
    /// </summary>
    /// <remarks>
    /// Get or set the default client prefix to use when creating new <see cref="MailKit.ProtocolLogger"/> instances.
    /// </remarks>
    /// <value>The default client prefix.</value>
    public static string DefaultClientPrefix
    {
        get { return Encoding.UTF8.GetString(defaultClientPrefix); }
        set { defaultClientPrefix = Encoding.UTF8.GetBytes(value); }
    }

    /// <summary>
    /// Get or set the default server prefix to use when creating new <see cref="MailKit.ProtocolLogger"/> instances.
    /// </summary>
    /// <remarks>
    /// Get or set the default server prefix to use when creating new <see cref="MailKit.ProtocolLogger"/> instances.
    /// </remarks>
    /// <value>The default server prefix.</value>
    public static string DefaultServerPrefix
    {
        get { return Encoding.UTF8.GetString(defaultServerPrefix); }
        set { defaultServerPrefix = Encoding.UTF8.GetBytes(value); }
    }

    /// <summary>
    /// Get or set the client prefix to use when logging client messages.
    /// </summary>
    /// <remarks>
    /// Gets or sets the client prefix to use when logging client messages.
    /// </remarks>
    /// <value>The client prefix.</value>
    public string ClientPrefix
    {
        get { return Encoding.UTF8.GetString(clientPrefix); }
        set { clientPrefix = Encoding.UTF8.GetBytes(value); }
    }

    /// <summary>
    /// Get or set the server prefix to use when logging server messages.
    /// </summary>
    /// <remarks>
    /// Gets or sets the server prefix to use when logging server messages.
    /// </remarks>
    /// <value>The server prefix.</value>
    public string ServerPrefix
    {
        get { return Encoding.UTF8.GetString(serverPrefix); }
        set { serverPrefix = Encoding.UTF8.GetBytes(value); }
    }

    /// <summary>
    /// Get or set whether or not authentication secrets should be redacted.
    /// </summary>
    /// <remarks>
    /// Gets or sets whether or not authentication secrets should be redacted.
    /// </remarks>
    /// <value><c>true</c> if authentication secrets should be redacted; otherwise, <c>false</c>.</value>
    public bool RedactSecrets
    {
        get; set;
    }

    /// <summary>
    /// Get or set whether timestamps should be logged.
    /// </summary>
    /// <remarks>
    /// Gets or sets whether or not timestamps should be logged.
    /// </remarks>
    /// <value><c>true</c> if timestamps should be logged; otherwise, <c>false</c>.</value>
    public bool LogTimestamps
    {
        get; set;
    }

    /// <summary>
    /// Get or set the date and time serialization format that should be used when logging timestamps.
    /// </summary>
    /// <remarks>
    /// Gets or sets the date and time serialization format that should be used when logging timestamps.
    /// </remarks>
    /// <value>The date and time serialization format that should be used when logging timestamps.</value>
    public string TimestampFormat
    {
        get; set;
    }

    #region IProtocolLogger implementation

    /// <summary>
    /// Get or set the authentication secret detector.
    /// </summary>
    /// <remarks>
    /// Gets or sets the authentication secret detector.
    /// </remarks>
    /// <value>The authentication secret detector.</value>
    public IAuthenticationSecretDetector AuthenticationSecretDetector { get; set; }

    private static void ValidateArguments(byte[] buffer, int offset, int count)
    {
        if (buffer == null)
            throw new ArgumentNullException(nameof(buffer));

        if (offset < 0 || offset > buffer.Length)
            throw new ArgumentOutOfRangeException(nameof(offset));

        if (count < 0 || count > (buffer.Length - offset))
            throw new ArgumentOutOfRangeException(nameof(count));
    }

    private void Log(byte[] prefix, ref bool midline, byte[] buffer, int offset, int count, bool isClient)
    {
        int endIndex = offset + count;
        int index = offset;
        int start;

        while (index < endIndex)
        {
            start = index;

            while (index < endIndex && buffer[index] != (byte)'\n')
                index++;

            if (!midline)
            {
                if (LogTimestamps)
                {
                    var timestamp = Encoding.ASCII.GetBytes(DateTime.UtcNow.ToString(TimestampFormat, CultureInfo.InvariantCulture));
                    stream.Write(timestamp, 0, timestamp.Length);
                    stream.Write(Space, 0, Space.Length);
                }

                stream.Write(prefix, 0, prefix.Length);
            }

            if (index < endIndex && buffer[index] == (byte)'\n')
            {
                midline = false;
                index++;
            }
            else
            {
                midline = true;
            }

            if (isClient && RedactSecrets)
            {
                var secrets = AuthenticationSecretDetector.DetectSecrets(buffer, start, index - start);

                foreach (var secret in secrets)
                {
                    if (secret.StartIndex > start)
                        stream.Write(buffer, start, secret.StartIndex - start);
                    start = secret.StartIndex + secret.Length;
                    stream.Write(Secret, 0, Secret.Length);
                }
            }

            stream.Write(buffer, start, index - start);
        }

        stream.Flush();
    }

    /// <summary>
    /// Logs a connection to the specified URI.
    /// </summary>
    /// <remarks>
    /// Logs a connection to the specified URI.
    /// </remarks>
    /// <param name="uri">The URI.</param>
    /// <exception cref="System.ArgumentNullException">
    /// <paramref name="uri"/> is <c>null</c>.
    /// </exception>
    /// <exception cref="System.ObjectDisposedException">
    /// The logger has been disposed.
    /// </exception>
    /// <exception cref="System.IO.IOException">
    /// An I/O error occurred.
    /// </exception>
    public void LogConnect(Uri uri)
    {
        if (uri == null)
            throw new ArgumentNullException(nameof(uri));

        string message;

        if (LogTimestamps)
        {
            message = string.Format("{0} Connected to {1}\r\n", DateTime.UtcNow.ToString(TimestampFormat, CultureInfo.InvariantCulture), uri);
        }
        else
        {
            message = string.Format("Connected to {0}\r\n", uri);
        }

        var buf = Encoding.ASCII.GetBytes(message);

        if (clientMidline || serverMidline)
        {
            stream.WriteByte((byte)'\r');
            stream.WriteByte((byte)'\n');
            clientMidline = false;
            serverMidline = false;
        }

        stream.Write(buf, 0, buf.Length);
        stream.Flush();
    }

    /// <summary>
    /// Logs a sequence of bytes sent by the client.
    /// </summary>
    /// <remarks>
    /// <para>Logs a sequence of bytes sent by the client.</para>
    /// <para><see cref="LogClient(byte[], int, int)"/> is called by the <see cref="IMailService"/> upon every successful
    /// write operation to its underlying network stream, passing the exact same <paramref name="buffer"/>,
    /// <paramref name="offset"/>, and <paramref name="count"/> arguments to the logging function.</para>
    /// </remarks>
    /// <param name='buffer'>The buffer to log.</param>
    /// <param name='offset'>The offset of the first byte to log.</param>
    /// <param name='count'>The number of bytes to log.</param>
    /// <exception cref="System.ArgumentNullException">
    /// <paramref name="buffer"/> is <c>null</c>.
    /// </exception>
    /// <exception cref="System.ArgumentOutOfRangeException">
    /// <para><paramref name="offset"/> is less than zero or greater than the length of <paramref name="buffer"/>.</para>
    /// <para>-or-</para>
    /// <para>The <paramref name="buffer"/> is not large enough to contain <paramref name="count"/> bytes strting
    /// at the specified <paramref name="offset"/>.</para>
    /// </exception>
    /// <exception cref="System.ObjectDisposedException">
    /// The logger has been disposed.
    /// </exception>
    /// <exception cref="System.IO.IOException">
    /// An I/O error occurred.
    /// </exception>
    public void LogClient(byte[] buffer, int offset, int count)
    {
        ValidateArguments(buffer, offset, count);

        Log(clientPrefix, ref clientMidline, buffer, offset, count, true);
    }

    /// <summary>
    /// Logs a sequence of bytes sent by the server.
    /// </summary>
    /// <remarks>
    /// <para>Logs a sequence of bytes sent by the server.</para>
    /// <para><see cref="LogServer(byte[], int, int)"/> is called by the <see cref="IMailService"/> upon every successful
    /// read of its underlying network stream with the exact buffer that was read.</para>
    /// </remarks>
    /// <param name='buffer'>The buffer to log.</param>
    /// <param name='offset'>The offset of the first byte to log.</param>
    /// <param name='count'>The number of bytes to log.</param>
    /// <exception cref="System.ArgumentNullException">
    /// <paramref name="buffer"/> is <c>null</c>.
    /// </exception>
    /// <exception cref="System.ArgumentOutOfRangeException">
    /// <para><paramref name="offset"/> is less than zero or greater than the length of <paramref name="buffer"/>.</para>
    /// <para>-or-</para>
    /// <para>The <paramref name="buffer"/> is not large enough to contain <paramref name="count"/> bytes strting
    /// at the specified <paramref name="offset"/>.</para>
    /// </exception>
    /// <exception cref="System.ObjectDisposedException">
    /// The logger has been disposed.
    /// </exception>
    /// <exception cref="System.IO.IOException">
    /// An I/O error occurred.
    /// </exception>
    public void LogServer(byte[] buffer, int offset, int count)
    {
        ValidateArguments(buffer, offset, count);

        Log(serverPrefix, ref serverMidline, buffer, offset, count, false);
    }

    #endregion IProtocolLogger implementation

    #region IDisposable implementation

    /// <summary>
    /// Releases the unmanaged resources used by the <see cref="ProtocolLogger"/> and
    /// optionally releases the managed resources.
    /// </summary>
    /// <remarks>
    /// Releases the unmanaged resources used by the <see cref="ProtocolLogger"/> and
    /// optionally releases the managed resources.
    /// </remarks>
    /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources;
    /// <c>false</c> to release only the unmanaged resources.</param>
    protected virtual void Dispose(bool disposing)
    {
        if (disposing && !leaveOpen)
            stream.Dispose();
    }

    /// <summary>
    /// Releases all resource used by the <see cref="MailKit.ProtocolLogger"/> object.
    /// </summary>
    /// <remarks>Call <see cref="Dispose()"/> when you are finished using the <see cref="MailKit.ProtocolLogger"/>. The
    /// <see cref="Dispose()"/> method leaves the <see cref="MailKit.ProtocolLogger"/> in an unusable state. After calling
    /// <see cref="Dispose()"/>, you must release all references to the <see cref="MailKit.ProtocolLogger"/> so the garbage
    /// collector can reclaim the memory that the <see cref="MailKit.ProtocolLogger"/> was occupying.</remarks>
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    #endregion IDisposable implementation
}